July 24 2019
Also: If you need to see all the data points at the same time DON’T use animation
Thomas Lin Pedersen’s example from https://gganimate.com - (@thomasp85)
(Be open to the idea that you might not need animation to be most effective)
library(ggplot2) # make plots library(gganimate) # animate the plots library(gifski) # render gifs
Optional, maybe interesting packages
library(transformr) # additional smooth transformations library(patchwork) # arrange multiple plots in a single layout library(ggforce) # extended options for ggplot functionality
Using data on displaced persons living in South Africa, show patterns and trends in refugee movement over twenty-plus years.
Where do most refugees come from?
Is the number of refugees coming from the most common countries of origin stable?
Follow along with the notebook: https://github.com/skirmer/animating_dataviz/blob/master/ioslides_deck.Rmd
Data Source: UNHCR via data.world: UNHCR’s populations of concern residing in South Africa
Grab dataset of displaced persons living in South Africa
| X_date_year | X_country_residence | X_country_origin | X_population_type | X_affected |
|---|---|---|---|---|
| 1996 | South Africa | Angola | Refugees (incl. refugee-like situations) | 3876 |
| 1996 | South Africa | Angola | Returned refugees | 308 |
| 1996 | South Africa | Bangladesh | Refugees (incl. refugee-like situations) | 452 |
| 1996 | South Africa | China | Refugees (incl. refugee-like situations) | 469 |
Select the top 10 countries of origin for each year, apply some filters.
library(magrittr) library(dplyr) plotDT <- dtset %>% filter(X_country_origin != "Various/Unknown") %>% filter(X_population_type == "Refugees (incl. refugee-like situations)") %>% group_by(X_date_year) %>% mutate(rank = rank(-X_affected, ties.method = "first") * 1) %>% ungroup() %>% filter(rank <= 10) %>% data.frame()
| X_date_year | X_country_residence | X_country_origin | X_population_type | X_affected | rank |
|---|---|---|---|---|---|
| 1996 | South Africa | Angola | Refugees (incl. refugee-like situations) | 3876 | 1 |
| 1996 | South Africa | Bangladesh | Refugees (incl. refugee-like situations) | 452 | 9 |
| 1996 | South Africa | China | Refugees (incl. refugee-like situations) | 469 | 8 |
| 1996 | South Africa | Dem. Rep. of the Congo | Refugees (incl. refugee-like situations) | 2505 | 3 |
Group by country, bar height is affected persons
baseplot1 <- ggplot(plotDT,
aes(X_date_year,
group = X_country_origin,
fill = as.factor(X_country_origin),
color = as.factor(X_country_origin)))+
theme_bw()+
theme(legend.position = "bottom")+
geom_bar(aes(y = X_affected), stat = "identity", position = "dodge")
Ew. This is not effective.
Let’s make this!
baseplot2 <- baseplot1 + coord_flip(clip = "off", expand = FALSE, ylim = c(0, 50000))
Can’t use the same stub now.
baseplot3 <- ggplot(plotDT,
aes(x=rank,
group = X_country_origin,
fill = as.factor(X_country_origin),
color = as.factor(X_country_origin)))+
theme_bw()+
theme(legend.position = "bottom")+
geom_text(aes(y = 0, label = paste(X_country_origin, " ")), vjust = 0.2, hjust = 1) +
coord_flip(clip = "off", expand = FALSE, ylim = c(0, 50000)) +
geom_bar(aes(y = X_affected), stat = "identity", position = "identity")
Left side: grouped bar, sorted by year
Right side: not grouped bar, sorted by rank
If your stub has a theme() segment, applying a new one will overrule it.
baseplot3 <- baseplot3 +
theme(legend.position = "none",
axis.ticks.y = element_blank(),
axis.text.y = element_blank(),
axis.title.y = element_blank(),
plot.margin = margin(1,1,1,5, "cm"))
baseplot3 <- baseplot3 + scale_y_continuous(labels = scales::comma) + scale_x_reverse()
Now we have the different “frames” all layered on top of each other.
Revisit our goals:
Let’s try static options one last time just to be sure.
No.
Definitely not.
Literally add one more line of code to your ggplot object.
animp <- baseplot3 + transition_states(X_date_year)
It’s nice, but we can do better
Solving Problem 1 and 2: Added a descriptive title/label that indicate the year of the frame, label bars
animp <- baseplot3 +
geom_text(aes(y = X_affected,
label = as.character(X_affected)),
color = "black", vjust = 0.2, hjust = -.5)+
labs(title = "Refugees Residing in South Africa by Origin, {closest_state}"
, y="Affected Persons")+
transition_states(X_date_year)
Solving Problem 3: how do we want the animation elements to move?
Option: shrink and grow on exit and enter
animp <- baseplot3 +
geom_text(aes(y = X_affected,
label = as.character(X_affected)),
color = "black", vjust = 0.2, hjust = -.5)+
labs(title = "Refugees Residing in South Africa by Origin, {closest_state}"
, y="Affected Persons")+
transition_states(X_date_year)+
enter_grow() +
exit_shrink()
It’s interesting, but probably not serving the project objectives
Solving Problem 3: how do we want the animation elements to move?
Option: Ease between positions (moving on page, not exiting or entering)
animp <- baseplot3 +
geom_text(aes(y = X_affected,
label = as.character(X_affected)),
color = "black", vjust = 0.2, hjust = -.5)+
labs(title = "Refugees Residing in South Africa by Origin, {closest_state}"
, y="Affected Persons")+
transition_states(X_date_year)+
ease_aes('quartic-in-out')
Makes the transition speed change as it moves (functions may be cubic, quartic, etc)
Solving Problem 3: how do we want the animation elements to move?
Option: For fun, let’s try “back” to see a springier approach
animp <- baseplot3 +
geom_text(aes(y = X_affected,
label = as.character(X_affected)),
color = "black", vjust = 0.2, hjust = -.5)+
labs(title = "Refugees Residing in South Africa by Origin, {closest_state}"
, y="Affected Persons")+
transition_states(X_date_year)+
ease_aes('back-in-out')
Feels a little cartoony- interesting, but again perhaps not what we need
Solving Problem 3: how do we want the animation elements to move?
In addition to entry, exit, and transition easing:
animp <- baseplot3 +
geom_text(aes(y = X_affected,
label = as.character(X_affected)),
color = "black", vjust = 0.2, hjust = -.5)+
labs(title = "Refugees Residing in South Africa by Origin, {closest_state}"
, y="Affected Persons")+
transition_states(X_date_year,transition_length = 5,
state_length = c(rep(.25, 21), 20), wrap = FALSE)+
ease_aes('linear')+
enter_fade() +
exit_fade()
Slower pace feels smoother, and doesn’t insinuate that the last frame and the first flow into each other
transition_states(
X_date_year,
transition_length = 5,
state_length = c(rep(.25, 21), 20),
wrap = FALSE)+
Assign the state unit - here we use the year.
Transition length
State length
Wrap determines whether to apply transition smoothing between the end and restarting
Experiment with these settings to get the look you want!
animate(animp, fps = 10, duration = 20, width = 800, height = 450)
anim_save(filename = "final_race_plot2.gif")
Prioritize the transmission of information effectively
Make your plot serve the audience, don’t be fancy if it’s not helpful
Think carefully about transitions and speed
Get feedback and test your animation on naive viewers
https://ggplot2.tidyverse.org/
https://ggforce.data-imaginist.com/
https://stackoverflow.com/questions/53162821/animated-sorted-bar-chart-with-bars-overtaking-each-other/53163549 (Hat tip to Jon Spring for this awesome starting point for this kind of thing!)
Similar project in D3: https://observablehq.com/@johnburnmurdoch/bar-chart-race-the-most-populous-cities-in-the-world